跳到主要内容

07.leader-election

如果你是一个想了解领导者选举以及在 Kubernetes 中如何工作的人,那么我希望你觉得这篇文章有用。在这篇文章中,我们将讨论高可用性系统中领导者选举背后的想法,并探索 kubernetes/client-go 库,以便在 Kubernetes 控制器的上下文中理解它。

介绍

近年来,随着对可靠系统和基础设施的需求增加,“高可用性”一词越来越受欢迎。在分布式系统中,高可用性通常涉及最大化正常运行时间和使系统具有容错能力。高可用性中常见的做法是使用冗余来最大程度地减少单点故障。准备系统和服务以实现冗余可能就像在负载平衡器后面部署更多副本一样简单。尽管这样的配置可以适用于许多应用程序,但某些用例需要跨副本进行仔细协调才能使系统正常工作

一个很好的例子是当 Kubernetes 控制器部署为多个实例时。为了防止任何意外行为,领导者选举过程必须确保在副本中选出一个领导者,并且是唯一主动协调集群的领导者。其他实例应保持非活动状态,但已准备好在领导者实例发生故障时接管

Kubernetes 中的领导者选举

Kubernetes 中的领导者选举过程很简单。它从创建锁定对象开始,其中领导者定期更新当前时间戳,作为通知其他副本有关其领导的一种方式。这个锁对象可以是 Lease 、 ConfigMap 或 Endpoint ,也持有当前领导者的身份。如果领导者未能在给定的时间间隔内更新时间戳,则假定它已崩溃,这是非活动副本通过使用其身份更新锁来竞相获取领导权时。成功获得锁的 Pod 将成为新的领导者

在我们处理任何代码之前,让我们先看看这个过程的实际效果!

第一步是建立一个本地的 Kubernetes 集群。我将使用 KinD ,但请随意选择您选择的本地 k8s 发行版。

$ kind create cluster
Creating cluster "kind" ...
✓ Ensuring node image (kindest/node:v1.21.1) 🖼
✓ Preparing nodes 📦
✓ Writing configuration 📜
✓ Starting control-plane 🕹️
✓ Installing CNI 🔌
✓ Installing StorageClass 💾
Set kubectl context to "kind-kind"
You can now use your cluster with:
kubectl cluster-info --context kind-kind
Not sure what to do next? 😅 Check out https://kind.sigs.k8s.io/docs/user/quick-start/

我们将使用的示例应用程序可以在这里找到。它使用 kubernetes/client-go 来执行领导者选举。让我们在集群上安装应用程序

# Setup required permissions for creating/getting Lease objects
$ kubectl apply -f rbac.yaml
serviceaccount/leaderelection-sa created
role.rbac.authorization.k8s.io/leaderelection-role created
rolebinding.rbac.authorization.k8s.io/leaderelection-rolebinding created
# Create deployment
$ kubectl apply -f deploy.yaml
deployment.apps/leaderelection created

这应该创建一个包含 3 个 Pod(副本)的部署。如果等待几秒钟,应会看到它们处于 Running 状态。

❯ kubectl get pods
NAME READY STATUS RESTARTS AGE
leaderelection-6d5b456c9d-cfd2l 1/1 Running 0 19s
leaderelection-6d5b456c9d-n2kx2 1/1 Running 0 19s
leaderelection-6d5b456c9d-ph8nj 1/1 Running 0 19s

运行 Pod 后,让我们尝试查看它们在领导者选举过程中创建的 Lease lock 对象。

$ kubectl describe lease my-lease
Name: my-lease
Namespace: default
Labels: <none>
Annotations: <none>
API Version: coordination.k8s.io/v1
Kind: Lease
Metadata:
...
Spec:
Acquire Time: 2021-10-23T06:51:50.605570Z
Holder Identity: leaderelection-56457b6c5c-fn725
Lease Duration Seconds: 15
Lease Transitions: 0
Renew Time: 2021-10-23T06:52:45.309478Z

据此,我们目前的领导者 pod 是 leaderelection-56457bc5c-fn725 。让我们通过查看我们的 pod 日志来验证这一点。

# leader pod
$ kubectl logs leaderelection-56457b6c5c-fn725
I1023 06:51:50.605439 1 leaderelection.go:248] attempting to acquire leader lease default/my-lease...
I1023 06:51:50.630111 1 leaderelection.go:258] successfully acquired lease default/my-lease
I1023 06:51:50.630141 1 main.go:57] still the leader!
I1023 06:51:50.630245 1 main.go:36] doing stuff...
# inactive pods
$ kubectl logs leaderelection-56457b6c5c-n857k
I1023 06:51:55.400797 1 leaderelection.go:248] attempting to acquire leader lease default/my-lease...
I1023 06:51:55.412780 1 main.go:60] new leader is %sleaderelection-56457b6c5c-fn725
# inactive pod
$ kubectl logs leaderelection-56457b6c5c-s48kx
I1023 06:51:52.905451 1 leaderelection.go:248] attempting to acquire leader lease default/my-lease...
I1023 06:51:52.915618 1 main.go:60] new leader is %sleaderelection-56457b6c5c-fn725

尝试删除领导者 Pod 以模拟崩溃,并检查 Lease 对象是否选出了新的领导者;)

代码分析

这里的基本思想是使用分布式锁定机制来决定哪个进程成为领导者。获取锁的过程可以执行所需的任务。 main 函数是我们应用程序的入口。在这里,我们创建一个对锁定对象的引用,并启动一个领导者选举循环。

func main() {
var (
leaseLockName string
leaseLockNamespace string
podName = os.Getenv("POD_NAME")
)
flag.StringVar(&leaseLockName, "lease-name", "", "Name of lease lock")
flag.StringVar(&leaseLockNamespace, "lease-namespace", "default", "Name of lease lock namespace")
flag.Parse()

if leaseLockName == "" {
klog.Fatal("missing lease-name flag")
}
if leaseLockNamespace == "" {
klog.Fatal("missing lease-namespace flag")
}

config, err := rest.InClusterConfig()
client = clientset.NewForConfigOrDie(config)

if err != nil {
klog.Fatalf("failed to get kubeconfig")
}

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

lock := getNewLock(leaseLockName, podName, leaseLockNamespace)
runLeaderElection(lock, ctx, podName)
}

我们首先解析 lease-name 和 lease-namespace 标志,以获取副本必须使用的锁定对象的名称和命名空间。 POD_NAME 环境变量的值(填充在 deploy.yaml 清单中)将用于标识 Lease 对象中的领导者。最后,我们使用这些参数创建一个锁定对象来启动领导者选举过程

runLeaderElection 函数是我们通过调用 RunOrDie 来启动领导者选举循环的地方。我们给它传递一个 LeaderElectionConfig :

func runLeaderElection(lock *resourcelock.LeaseLock, ctx context.Context, id string) {
leaderelection.RunOrDie(ctx, leaderelection.LeaderElectionConfig{
Lock: lock,
ReleaseOnCancel: true,
LeaseDuration: 15 * time.Second,
RenewDeadline: 10 * time.Second,
RetryPeriod: 2 * time.Second,
Callbacks: leaderelection.LeaderCallbacks{
OnStartedLeading: func(c context.Context) {
doStuff()
},
OnStoppedLeading: func() {
klog.Info("no longer the leader, staying inactive.")
},
OnNewLeader: func(current_id string) {
if current_id == id {
klog.Info("still the leader!")
return
}
klog.Info("new leader is %s", current_id)
},
},
})
}

现在,让我们看一下 RunOrDie 在 client-go 中的实现。

// RunOrDie starts a client with the provided config or panics if the config
// fails to validate. RunOrDie blocks until leader election loop is
// stopped by ctx or it has stopped holding the leader lease
func RunOrDie(ctx context.Context, lec LeaderElectionConfig) {
le, err := NewLeaderElector(lec)
if err != nil {
panic(err)
}
if lec.WatchDog != nil {
lec.WatchDog.SetLeaderElection(le)
}
le.Run(ctx)
}

它使用我们传递给它的 LeaderElectorConfig 创建一个 *LeaderElector ,并在其上调用 Run 方法:

// Run starts the leader election loop. Run will not return
// before leader election loop is stopped by ctx or it has
// stopped holding the leader lease
func (le *LeaderElector) Run(ctx context.Context) {
defer runtime.HandleCrash()
defer func() {
le.config.Callbacks.OnStoppedLeading()
}()

if !le.acquire(ctx) {
return // ctx signalled done
}
ctx, cancel := context.WithCancel(ctx)
defer cancel()
go le.config.Callbacks.OnStartedLeading(ctx)
le.renew(ctx)
}

此方法负责运行领导者选举循环。它首先尝试获取锁(使用 le.acquire )。成功后,它会运行我们之前配置的 OnStartedLeading 回调,并定期续订租约。如果无法获取锁,它只需运行 OnStoppedLeading 回调并返回

acquire 和 renew 方法实现中最重要的部分是对 tryAcquireOrRenew 的调用,它包含锁定机制的核心逻辑。

乐观锁定(并发控制)

领导者选举过程利用 Kubernetes 操作的原子性质来确保没有两个副本可以同时获取 Lease (否则可能会导致竞争条件和其他意外行为!每当 Lease 更新(更新或获取)时,其上的 resourceVersion 字段也会由 Kubernetes 更新。当另一个进程尝试同时更新 Lease 时,Kubernetes 会检查更新对象的 resourceVersion 字段是否与当前对象匹配 — 如果没有,更新将失败,从而防止并发问题!

总结

在这篇文章中,我们介绍了领导者选举的概念,以及为什么它对于分布式系统的高可用性至关重要。我们看了一下如何使用 Lease 锁在 Kubernetes 中实现这一点,并尝试使用 kubernetes/client-go 库自行实现它。此外,我们还试图了解 Kubernetes 如何使用原子操作和乐观锁定方法来防止由并发引起的问题。